/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the ViewMyCurrency extension.
 *
 * The Initial Developer of the Original Code is
 * Will Moffat.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

//dump("// $Id: WILL_HtmlConversion.js,v 1.44 2007/03/19 10:54:28 Will Exp $;\n");

// WILL: The javascript OOP is different here than in NumberConversion. Here I create a singelton object, there I use prototypes
// I wonder which technique is better?

VMC.HtmlConversion = new function() {
  var self = this;

this.NODE_START_CHAR  = '\v'; //vertical-tab - counts as whitespace
this.NODE_START_CHAR_RE  = /\v/g; // for matching this char
this.ELEM_END_CHAR    = '\f'; //form-feed WILL: We assume no vertical-tabs or form-feeds in the HTML. WILL: TODO: add check
this.ELEM_END_CHAR_RE = /\f/g; // for globally removing the end chars


// sometimes documents do not have a location, for example about:blank created with window.open()  is this a FF BUG?
this.getURL = function(docObj) {
  if (!docObj) { VMC.alert('null docObj'); }
  if (docObj.location) { return docObj.location.href; }
  return docObj.URL || docObj.documentURI;
};    

this.restorePage = function(docObj,conversionProfileTable) {

  var activeProfileEntry = this.getActiveProfileEntry(docObj,conversionProfileTable);
  if (!activeProfileEntry.profile) {
    return;
  }

  var range = { startContainer:docObj.body, endContainer:docObj.body };
  var nodes   = this._getAllNodesInRange(range);
  var oldText = this._getTextOfNodes(nodes);

  var oldHighlightChanges = this.highlightChanges;
  this.highlightChanges = false; // temporarily disable highlight changes
  this._setTextOfNodes(nodes,oldText);
  this.highlightChanges = oldHighlightChanges; // restore setting

  if (oldHighlightChanges) { // remove highlight changes
    for (var i=0;i<nodes.length;i++) {
      if (nodes[i].VMC_origOutline) {  VMC.removeOutline(nodes[i]); }
    }
  }

  return;
};

  this.getActiveProfileEntry = function(docObj,conversionProfileTable) {
    var url = this.getURL(docObj);
    var activeProfileEntry = conversionProfileTable.setActiveProfileEntryForUrl(url); //WILL: TODO: too much duplication with restorepage
    return activeProfileEntry;
  };

    
// starts threads which convert the page. calls notifyFunc if page was changed
////////////////////////////////
  this.changePageToTargetCurrencyUsingNodes = function(docObj,targetCurrency, hideOriginal, conversionProfileTable, getRateFunc, notifyFunc) {
  ///////////////////////////////
  var activeProfileEntry = this.getActiveProfileEntry(docObj,conversionProfileTable);

  //vmc.hack(VMC.escapeUnicode(activeProfileEntry.toString()));

  if (!activeProfileEntry.profile) {
    notifyFunc('AVOID'); //this page is excluded
    //vmc.hack('avoiding '+docObj.location);
    return;
  }
    
  var range = { startContainer:docObj.body, endContainer:docObj.body };

  var nodes;
  var oldText;
  var newText;

  var getNodesFunc     = function() { nodes   = self._getAllNodesInRange(range); };
  var getOldTextFunc   = function() { oldText = self._getTextOfNodes(nodes); };
  var convertTextFunc  = function() { newText = VMC.NumberConversion.prototype.currencyConvertStr(oldText,targetCurrency, activeProfileEntry.profile, getRateFunc, hideOriginal); }; 
  var setTextFunc      = function() {
    if (newText) { self._setTextOfNodes(nodes,newText); notifyFunc('END_CHANGES'); }
    else         {                                      notifyFunc('END_NO_CHNG'); }
  };

  setTimeout( getNodesFunc,    1);
  setTimeout( getOldTextFunc,  2);
  setTimeout( convertTextFunc, 3);
  setTimeout( setTextFunc,     4); //WILL: CHECK: this ensures the correct execution order?

//   VMC.log('DBG3','Original number of nodes = '+nodes.length);
//   VMC.log('DBG3','Original node length = '+oldText.split(this.NODE_START_CHAR).length+' content = '+oldText);

//   if (!newText) {
//     VMC.log('INFO','No changes to make');
//     return false; //nothing to do
//   }
    
  // VMC.log('DBG3','New node length = '+newText.split(this.NODE_START_CHAR).length+' new content = '+newText);
};



//WILL: should this be private - used by PopupToolbar
// if offsets are undefined, then take use complete start and end node values
// Will first return the original value of the node if we set that while making changes. Otherwise return current value in DOM
// (if somebody else has edited a node that we had converted, their change will be lost)
// NOTE: this is not the same as node.textContent as getNodes() above has weeded out SCRIPT nodes.
this._getTextOfNodes = function(nodes,startOffset,endOffset) {
  var text='';
  var str;
  var i;
  var EMPTY='';
  if (!nodes.length) {VMC.log('WARN','no text nodes in range'); return null; }

  if (nodes.length==1) {
    //  ************ single node range ***************
    // first node:
    str=nodes[0].oldValue || nodes[0].nodeValue || EMPTY;
    if (startOffset===undefined)  { text += str; }
    else                          { text += str.substr(startOffset,endOffset-startOffset); }

  } else {

    //  *************** multiple nodes ****************
    // first node:
    str=nodes[0].oldValue || nodes[0].nodeValue || EMPTY;
    if (startOffset===undefined) { text += str; }
    else                         { text += str.substr(startOffset,str.length-startOffset); } // rightStr
    for (i=1; i < nodes.length-1 ; i++) {
      // middle nodes:
      text += this.NODE_START_CHAR + (nodes[i].oldValue || nodes[i].nodeValue || EMPTY);
      if (nodes[i].nextSibling === null ) { text += this.ELEM_END_CHAR; } // we have reached the last child of the current element
    }
    // last node:
    str=nodes[i].oldValue || nodes[i].nodeValue || EMPTY;
    if (endOffset===undefined)  { text += this.NODE_START_CHAR + str; }
    else                        { text += this.NODE_START_CHAR + str.substr(0,endOffset); } // leftStr
 
  } 
    
  return text;
};
    

// only used in testing
this._diffTextOfNodes = function(nodes,newText) {
  var str;
  var newValue;
  var oldValue;
  var i;
  newText = newText.replace(this.ELEM_END_CHAR_RE,''); //remove the element end markers - they are only used for giving the correct string lengths when calculting which number is closest to which currency symbol
  var newNodesContent=newText.split(this.NODE_START_CHAR);

  //  *************** multiple nodes ****************
  // we check only the
  // middle nodes:
    for (i=1; i < nodes.length-1 ; i++) {  
      newValue=newNodesContent[i] || ''; //convert null to ''  WILL: TODO: how can this be null too?
      oldValue=nodes[i].nodeValue || ''; //convert null to ''
      if (newValue != oldValue) {
	dump('nodes['+i+'] diff: '+
	     'NEW: ['+VMC.shortenStr(newValue,60)+']   '+
	     'OLD: ['+VMC.shortenStr(oldValue,60)+']\n');
      }
    }
    //TODO: look at DOMSubTreeModified mutation event
};



      
      
// I tried to use attributes to store the original values but Text nodes don't have attributes. And there can be more than one text node per parent node so I just just store it in the parent.
this._changeNode = function(node,newValue) {
  var attr;
  var store;
  var oldValue=node.nodeValue;
  if (oldValue != newValue) {
    //dump('changing node from ['+VMC.shortenStr(oldValue,30)+'] to ['+VMC.shortenStr(newValue,30)+']\n');
    node.nodeValue=newValue;
    if (this.highlightChanges) {  VMC.setOutline(node.parentNode,'solid thin red'); } // show changes on the web page
    if (node.oldValue) {
      //we only allow resetting to the original value, not undoing the last conversion
      //dump('WILL: not changing oldValue as it was already set to '+VMC.shortenStr(node.oldValue,30)+'\n\n');
    } else {
      node.oldValue=oldValue; //record the change
    }
  }
};

this._setTextOfNodes = function(nodes,newText,startOffset,endOffset, forceUpdateEmpty) {
  var str;
  var newValue;
  var leftStr;
  var rightStr;
  var i;
  var EMPTY = '';
  newText = newText.replace(this.ELEM_END_CHAR_RE,''); //remove the element end markers - they are only used for giving the correct string lengths when calculting which number is closest to which currency symbol
  var newNodesContent=newText.split(this.NODE_START_CHAR);
  if (newNodesContent.length != nodes.length) {
    VMC.log('ERR',' We have new content for '+newNodesContent.length+' nodes but there are '+nodes.length+' nodes to fill!');
    this._diffTextOfNodes(nodes,newText);
  }

  //VMC.log('WARN',nodes.join(','));
  //this._setNodesInRange(range,newNodeContent);

  if (nodes.length==1) {
    //  ************ single node range ***************
    // first node:
    newValue=newNodesContent[0];
    str=nodes[0].nodeValue || EMPTY;
    if (newValue ||  forceUpdateEmpty) {
      if (startOffset===undefined) {
	this._changeNode(nodes[0],newValue);
      } else {
	leftStr  = str.substr(0,startOffset);
	rightStr = str.substr(endOffset,str.length-endOffset);
	this._changeNode(nodes[0], leftStr + newValue + rightStr);
      }
    }
  } else {
    //  *************** multiple nodes ****************
    // first node:
    newValue=newNodesContent[0];
    if (newValue  ||  forceUpdateEmpty) {
      if (startOffset===undefined) {
	this._changeNode(nodes[0],newValue);
      } else {
	str=nodes[0].nodeValue || EMPTY;
	this._changeNode(nodes[0], str.substr(0,startOffset) + newValue); // leftStr
      }
    }
    // middle nodes:
    for (i=1; i < nodes.length-1 ; i++) {  
      newValue=newNodesContent[i];
      if (newValue  ||  forceUpdateEmpty) {
	this._changeNode(nodes[i],newValue);
      }
    }
    // last node:
    newValue=newNodesContent[i];
    if (newValue  ||  forceUpdateEmpty) {
      if (endOffset===undefined) {
	this._changeNode(nodes[i],newValue);
      } else {
	str=nodes[i].nodeValue || EMPTY;
	this._changeNode(nodes[i], newValue + str.substr(endOffset,str.length-endOffset)); // rightStr
      }
    }
  }
  //WILL: TODO: if (newValue && newValue != old)   + store old value!
  //WILL: TODO: help the GC
};



      
// this tree-walker returns all nodes in the range, except for
// skip: scripts, <pre> and <code> sections (often these are code examples with $ signs),  <!-- HTML comments -->      
//WILL: TODO: should this be private? It's used by PopupConvertToolbarManager
//W: if (currentNode.nodeType == 3) { //WILL: We don't do this because we need the HTML tags to estimate which symbol is closer to a number
//WILL: TODO: investigate using XPATH for this??
//WILL: TODO: range.detach() to improve memory performance?
this._getAllNodesInRange = function(range) {
  var endNode     = range.endContainer;
  var startNode   = range.startContainer;
  var textNodes   = new Array(1000); // optimisation, start with 1000 elements and increase if necessary
  var index       = 0;
  var n;              // currentNode
  var nn = startNode; //nextNode
  var skipNode;
  var name;
  var visitingParent = false; // visiting parent node, so don't go back to child again
  var stopNode = endNode.nextSibling || endNode.parentNode || null; // stop when this would be our next node  
  var visitedTimeStamp = new Date(); // this func can be called multiple times on the same node range, so we need a unique identifier
  do {
    n = nn;
    skipNode = (n.VMC_visited && n.VMC_visited == visitedTimeStamp); // have we visited this node already?
    n.VMC_visited = visitedTimeStamp;

    if (!skipNode) {
      name = n.nodeName;
      skipNode = (name=="SCRIPT" || name=="PRE" || name=="CODE" || n.nodeType==8);
    }

    if (!skipNode) {
      textNodes[index]=n;
      index++;
    }

    if (!skipNode && n.firstChild)  { nn = n.firstChild;  }
    else if         (n.nextSibling) { nn = n.nextSibling; }
    else                            { nn = n.parentNode; }
  } while (!(n === endNode && nn === stopNode));

  textNodes.length = index; //shrinks the array if necessary
  //vmc.hack('node count: '+textNodes.length);
  return textNodes;
};

//WILL: This was just for debugging. It's far too expensive for a large page. if (arrayContainsDuplicates(textNodes)) { VMC.alert('array contains duplicates '+textNodes); } 
//   function arrayContainsDuplicates(a) {
//     var isDupFunc = function(e) {
//       var isDup = (a.indexOf(e) != a.lastIndexOf(e) );
//       if (isDup) { vmc.hack('DUP: '+e); }
//       return isDup;
//     };
//     return a.some(isDupFunc); 
//   }

//WILL: TODO: If I want to check for marker chars in the original HTML then put this in the while loop (it's slow!)
//     if (n.nodeValue) {
//       var text=n.nodeValue;
//       if (this.countMarkerChars(text)) {
// 	dump('ERROR: Marker found in '+VMC.shortenStr(text,40)+'\n');
//       }
//     }

this.countMarkerChars = function(text) {
  var markers=0;
  for (var i=0;i<text.length;i++) {
    if (text[i]==this.NODE_START_CHAR || text[i]==this.ELEM_END_CHAR) {
      markers++;
    }
  }
  return markers;
};




  
  // returns targetNode text + specified numbers of chars from nodes to left and right 
  this._getStrLeftRight = function(targetNode,lengthLeft,lengthRight /*,x,y */) {
    var ancestorNode = targetNode;
    var foundEnoughChars;
    var r;
    do {
      ancestorNode = ancestorNode.parentNode;
      r = this._getNodesLeftRight(targetNode,ancestorNode /*,x,y */);
      foundEnoughChars = r && (r.leftStr.length >= lengthLeft) && (r.rightStr.length >= lengthRight);
    }
    while (ancestorNode.parentNode && !foundEnoughChars);
    
    //WILL: this can happen at the if document.body is the targetNode if (!foundEnoughChars) { VMC.log('ERR','Could not find enought chars'); }

    if (!r) { return null; }

    lengthLeft  = this._expandLeft(  r.leftStr,  lengthLeft  );
    lengthRight = this._expandRight( r.rightStr, lengthRight );

    var leftStr  = r.leftStr.substr(-lengthLeft,lengthLeft+1); // WILL: TODO: claify, we are treating this as a position, not as a length
    var rightStr = r.rightStr.substr(0,lengthRight+1);
    return { leftStr:leftStr, rightStr:rightStr };
  };    

  this._expandLeft = function(text,startIndex) {
    var numberRE = /[\d\s\,\.]/; // valid chars in a number
    var newIndex=startIndex;
    while ( newIndex>0 && numberRE.test(text[newIndex-1]) ) { //test each char to the left of the startIndex
    //while(newIndex<text.length && numberRE.test(text.substr(-newIndex-1,1))) { //test each char to the left of the startIndex
      newIndex--;
    }
    return newIndex; 
  };

  this._expandRight = function(text,startIndex) {
    var numberRE = /[\d\s\,\.]/; // valid chars in a number
    var newIndex=startIndex;
    //while(newIndex<text.length && numberRE.test(text.substr(newIndex+1,1))) {
    while ( newIndex<(text.length-1) && numberRE.test(text[newIndex+1]) ) {
      newIndex++;
    }
    return newIndex; 
  };

//   // from http://www.quirksmode.org/js/findpos.html
//   function findPos(obj) {
//     var curleft = curtop = 0;
//     if (obj.offsetParent) {
//       curleft = obj.offsetLeft
// 	curtop = obj.offsetTop
// 	while (obj = obj.offsetParent) {
// 	  curleft += obj.offsetLeft
// 	  curtop += obj.offsetTop
// 	}
//     }
//     return [curleft,curtop];
//   }

  this._getNodesLeftRight = function(middleNode,ancestorNode /*,x,y */) {
    var EMPTY = '';
    var range = { startContainer:ancestorNode, endContainer:ancestorNode };
    var nodes = VMC.HtmlConversion._getAllNodesInRange(range);
    var leftStr='';
    var rightStr='';
    var foundMiddle = false;
    for (var i=0;i<nodes.length;i++) {
      var node = nodes[i];
      if (node === middleNode) { foundMiddle = true; }
      //WILL: TODO: If node's position is too far away from (x,y) then ignore it
      else {
	if (foundMiddle) { rightStr += (node.nodeValue || EMPTY) + this.NODE_START_CHAR; }
	else             { leftStr  += (node.nodeValue || EMPTY) + this.NODE_START_CHAR; }
      }
    }
    if (!foundMiddle) { return null; } // this happens if we move over a text edit box (since it isn't included in the node list)
    return { rightStr:rightStr, leftStr:leftStr };
  };

  this.findTextUnderMouse = function(event) {
    var targetNode = event.rangeParent;
    var offset = event.rangeOffset;

//     var x = -1000; //WILL: TODO: this needs to be set by the event.node??
//     var y = -1000; 
    var spaceRE = /\s/;

    if (targetNode === null || targetNode.nodeType != Node.TEXT_NODE) { return null; } // not a text node
    var str = targetNode.nodeValue; // = targetNode.oldValue; // WILL: TODO: could we also use tooltips on converted pages? 
    if (offset < 0 || offset >= str.length)                          { return null; } // <0 or >str.length means ??,   =str.length - pointer at end of line

    // is this targetNode too far away from the mouse click x,y ?
//     var n = event.originalTarget //WILL: can we use the TextNode of explicitOriginalTarget?? It doesn't seem to have a position
//     if (n.offsetParent) {
//        var nx=n.offsetParent.offsetRight + n.offsetRight;
//        var dx=Math.abs(event.screenX-nx);
//        var distanceTooBig = (dx>100); //HACK: this depends on the size of the text!
//        VMC.HACK('targetNode='+n+' distanceTooBig='+distanceTooBig);
//        if (distanceTooBig) { return null; }
//      }
    // Extract the keyword.

    // if we are over a number then expand the start point to completely enclose it
    var start = this._expandLeft(str,offset);
    var end   = this._expandRight(str,offset); 

    //  increase the window by 5 chars to the left and right
    start -= 5; 
    end   += 5;
    var charsMissingLeft = 0;
    var charsMissingRight = 0;

    var lastCharPos = str.length-1;
    if (start<0)         { charsMissingLeft = -start;           start=0;         } else { start = this._expandLeft(str,start); /* grow left ?? */ }
    if (end>lastCharPos) { charsMissingRight = end-lastCharPos; end=lastCharPos; } else { end   = this._expandRight(str,end); }

    var windowStr = str.substring(start, end+1);
    if (charsMissingLeft || charsMissingRight) {
       var r = this._getStrLeftRight(targetNode,charsMissingLeft,charsMissingRight /*,x,y */);
      if (!r) { return null; }
      if (charsMissingLeft)  { windowStr = r.leftStr + '\v' + windowStr;  }
      if (charsMissingRight) { windowStr = windowStr + '\v' + r.rightStr; }
    }
    
    return windowStr;
  };


this.highlightChanges = true; // use style to highlight changes we make on the web page?

}; // end of HtmlConversion
